package com.smsc.headend.task.engine.service.collection.impl;

import cn.hutool.core.convert.Convert;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.smsc.headend.common.constant.CacheNameConstant;
import com.smsc.headend.common.utils.RedisKeys;
import com.smsc.headend.common.utils.UDateUtils;
import com.smsc.headend.module.data.entity.HdCollMeterIntegrity;
import com.smsc.headend.module.protocol.cosem.util.ByteUtils;
import com.smsc.headend.module.template.loadprofile.entity.TemplateLoadProfileEntity;
import com.smsc.headend.task.engine.mapper.HdCollMeterIntegrityMapper;
import com.smsc.headend.task.engine.service.FeignAssetManagementService;
import com.smsc.headend.task.engine.service.collection.IntegrityService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author liangli
 * @date 2020/12/7
 */
@Service
@CacheConfig(cacheNames = CacheNameConstant.UTE_INTEGRITY)
@Slf4j
public class IntegrityServiceImpl extends ServiceImpl<HdCollMeterIntegrityMapper, HdCollMeterIntegrity> implements IntegrityService {

    public static final int MAX_LINE_1000COUNT = 1000;
    @Autowired
    RedisTemplate<String, String> redisTemplate;
    @Autowired
    FeignAssetManagementService assetManagementService;

    Cache<String, Integer> meterLpIntervalCache = CacheBuilder.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS).build();
    Cache<String, Integer> meterLpTimelyCache = CacheBuilder.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS).build();

    @Override
    @Cacheable(key = "#meterId + '.' + #tvOfDay + '.' + #lpDataItemId + '.' + #intervalTime", unless = "#result == null")
    public HdCollMeterIntegrity getHdCollMeterIntegrity(Long meterId, Long tvOfDay, Long lpDataItemId, Integer intervalTime) {
        return lambdaQuery()
                .eq(HdCollMeterIntegrity::getMeterId, meterId)
                .eq(HdCollMeterIntegrity::getTv, tvOfDay)
                .eq(HdCollMeterIntegrity::getLpDataItemId, lpDataItemId)
//                .eq(HdCollMeterIntegrity::getDataItemId, dataItemId)
                .eq(HdCollMeterIntegrity::getCollInterval, intervalTime).one();
    }

    @Override
    @CachePut(key = "#hdCollMeterIntegrity.meterId + '.' + #hdCollMeterIntegrity.tv + '.' + #hdCollMeterIntegrity.lpDataItemId + '.' + #hdCollMeterIntegrity.collInterval", unless = "#result == null")
    public HdCollMeterIntegrity insert(HdCollMeterIntegrity hdCollMeterIntegrity) {
        this.save(hdCollMeterIntegrity);
        return hdCollMeterIntegrity;
    }

    @Override
    @CachePut(key = "#hdCollMeterIntegrity.meterId + '.' + #hdCollMeterIntegrity.tv + '.' + #hdCollMeterIntegrity.lpDataItemId + '.' + #hdCollMeterIntegrity.collInterval", unless = "#result == null")
    public HdCollMeterIntegrity updateIntegrity(HdCollMeterIntegrity hdCollMeterIntegrity) {
        this.updateById(hdCollMeterIntegrity);
        return hdCollMeterIntegrity;
    }

    @Override
    public List<HdCollMeterIntegrity> getHdCollMeterIntegrityList(Long meterId, Long tvOfDay, Long lpDataItemId) {
        return lambdaQuery().eq(HdCollMeterIntegrity::getMeterId, meterId)
                .eq(HdCollMeterIntegrity::getTv, tvOfDay)
                .eq(HdCollMeterIntegrity::getLpDataItemId, lpDataItemId).list();
    }

    @Override
    public String getIntegrityDetailInCache(Long meterId, Long tvOfDay, Long lpDataItemId, Integer intervalTime) {
        String redisKey = String.format("%s:%s:%s:%s", meterId, lpDataItemId, intervalTime, tvOfDay);
        String meterLoadProfileIntegrityKey = RedisKeys.getMeterLoadProfileIntegrity(redisKey);
        //获取字符串的长度
        String i = redisTemplate.opsForValue().get(meterLoadProfileIntegrityKey);
        if (StringUtils.isEmpty(i)) {
            log.info("get redis integrity empty: key={}", redisKey);
            return null;
        }
        char[] carr = i.toCharArray();
        byte[] dd = new byte[carr.length];

        AtomicInteger offset = new AtomicInteger(0);
        //获取每个位的bool值
        List<Object> results = redisTemplate.executePipelined((RedisCallback<String>) redisConnection -> {
            for (int q = 0; q < carr.length * 8; q++) {
                redisConnection.getBit(meterLoadProfileIntegrityKey.getBytes(StandardCharsets.UTF_8), offset.getAndIncrement());
            }
            return null;
        });
        //组成byte[]
        for (int q = 0; q < carr.length; q++) {
            dd[q] = 0;
            for (int j = 0; j < 8; j++) {
                Boolean bit = (Boolean) results.get(q * 8 + j);
                dd[q] = (byte) (dd[q] | ((bit ? 1 : 0) << j));
            }
        }
        //返回hex字符串
        return ByteUtils.byteToHexString(dd).replaceAll(" ", "");
    }


    @Override
    public void meterLpDataSaveToCache(Map<String, Set<Long>> meterLpDataTvMap) {
        log.debug("save integrity: size={}, content={}", meterLpDataTvMap.keySet().size(), JSONUtil.toJsonStr(meterLpDataTvMap.keySet()));
        List<Map<String, Set<Integer>>> updateLCollList = new ArrayList<>();
        List<Map<String, Set<Integer>>> updateLTimelyList = new ArrayList<>();
        for (Map.Entry<String, Set<Long>> entry : meterLpDataTvMap.entrySet()) {
            String meterIdLpDataItemIdKey = entry.getKey();
            Integer intervalTime = meterLpIntervalCache.getIfPresent(meterIdLpDataItemIdKey);
            Integer spaceOfTime = meterLpTimelyCache.getIfPresent(meterIdLpDataItemIdKey);
            String[] info = meterIdLpDataItemIdKey.split(":");
            Long meterId = Convert.toLong(info[0]);
            Long lpDataItemId = Convert.toLong(info[1]);
            if (intervalTime == null) {
                TemplateLoadProfileEntity entity = assetManagementService.getTemplateLoadProfileEntity(meterId, lpDataItemId);
                if (null == entity || null == entity.getIntervalTime() || StringUtils.isBlank(entity.getIntervalType())) {
                    log.error("getTemplateLoadProfileEntity error!");
                    continue;
                }
                intervalTime = UDateUtils.getPeriodSecByType(entity.getIntervalTime().intValue(), entity.getIntervalType());
                spaceOfTime = this.getSpaceOfTime(entity);
            }
            meterLpIntervalCache.put(meterIdLpDataItemIdKey, intervalTime);
            meterLpTimelyCache.put(meterIdLpDataItemIdKey, spaceOfTime);
            //get key set map for redis update
            Map<String, Set<Integer>> updateCollSetMap = cacheMinutelyIntegrity(meterLpDataTvMap, meterIdLpDataItemIdKey, intervalTime);
            Map<String, Set<Integer>> updateTimelySetMap = cacheMinutelyTimely(meterLpDataTvMap, meterIdLpDataItemIdKey, intervalTime, spaceOfTime);
            updateLCollList.add(updateCollSetMap);
            updateLTimelyList.add(updateTimelySetMap);
        }
        saveLpIntegrityToRedis(updateLCollList);
        saveLpTimelyToRedis(updateLTimelyList);
    }

    /**
     * 获取配置计算及时率的时间间隔
     *
     * @param templateLoadProfileEntity
     * @return
     */
    public Integer getSpaceOfTime(TemplateLoadProfileEntity templateLoadProfileEntity) {
        Integer defaultTime = 90;
        if (null == templateLoadProfileEntity) {
            return defaultTime;
        }
        String configTimely = null;
        if (TemplateLoadProfileEntity.IntervalType.MINUTELY.getType().equals(templateLoadProfileEntity.getIntervalType())) {
            configTimely = assetManagementService.getPropertyValue("PROFILE_TIMELY_RATIO_MINUTELY");
        } else if (TemplateLoadProfileEntity.IntervalType.DAILY.getType().equals(templateLoadProfileEntity.getIntervalType())) {
            configTimely = assetManagementService.getPropertyValue("PROFILE_TIMELY_RATIO_DAILY");
        } else if (TemplateLoadProfileEntity.IntervalType.MONTHLY.getType().equals(templateLoadProfileEntity.getIntervalType())) {
            configTimely = assetManagementService.getPropertyValue("PROFILE_TIMELY_RATIO_MONTHLY");
        }
        Map<Integer, Integer> map = new HashMap<>(8);
        try {
            if (StringUtils.isNotBlank(configTimely)) {
                String[] s = configTimely.split(";");
                if (null != s) {
                    for (String one : s) {
                        String[] ones = one.split(",");
                        if (null != ones) {
                            map.put(Integer.parseInt(ones[0]), Integer.parseInt(ones[1]));
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error("The format of the configuration is incorrect!");
            return defaultTime;
        }
        Integer intervalTime = Math.toIntExact(templateLoadProfileEntity.getIntervalTime());
        if (null != map.get(intervalTime)) {
            return map.get(intervalTime);
        }
        return defaultTime;
    }

    /**
     * 保存完整率详情至Redis
     *
     * @param updateList
     */
    private void saveLpIntegrityToRedis(List<Map<String, Set<Integer>>> updateList) {
        redisTemplate.executePipelined((RedisCallback<Object>) redisConnection -> {
            updateList.forEach(map -> map.forEach((redisKey, value) -> {
                for (Integer offset : value) {
                    redisConnection.setBit(RedisKeys.getMeterLoadProfileIntegrity(redisKey).getBytes(StandardCharsets.UTF_8), offset, true);
                }
                redisConnection.expire(RedisKeys.getMeterLoadProfileIntegrity(redisKey).getBytes(StandardCharsets.UTF_8), DateUtils.MILLIS_PER_DAY);
                redisConnection.setCommands().sAdd(RedisKeys.getIntegrityDateSet().getBytes(StandardCharsets.UTF_8), redisKey.getBytes(StandardCharsets.UTF_8));
            }));
            return null;
        });
    }

    /**
     * 保存及时率详情至Redis
     *
     * @param updateList
     */
    private void saveLpTimelyToRedis(List<Map<String, Set<Integer>>> updateList) {
        redisTemplate.executePipelined((RedisCallback<Object>) redisConnection -> {
            updateList.forEach(map -> map.forEach((redisKey, value) -> {
                for (Integer offset : value) {
                    redisConnection.setBit(RedisKeys.getMeterLoadProfileTimely(redisKey).getBytes(StandardCharsets.UTF_8), offset, true);
                }
                redisConnection.expire(RedisKeys.getMeterLoadProfileTimely(redisKey).getBytes(StandardCharsets.UTF_8), DateUtils.MILLIS_PER_DAY);
            }));
            return null;
        });
    }

    public Map<String, Set<Integer>> cacheMinutelyIntegrity(Map<String, Set<Long>> meterLpDataTvMap, String meterIdLpDataItemIdKey, Integer intervalTime) {
        Set<Long> dataTvs = meterLpDataTvMap.get(meterIdLpDataItemIdKey);
        log.debug("save integrity: tv size={}, dataTv={}", dataTvs.size(), JSONUtil.toJsonStr(dataTvs));
        Map<String, Set<Integer>> keyOffset = new HashMap<>();
        for (Long dataTv : dataTvs) {
            Date date = new Date(dataTv * 1000);
            Long tvOfDay = DateUtils.truncate(date, Calendar.DATE).getTime() / 1000;
            String redisKey = meterIdLpDataItemIdKey + ":" + intervalTime + ":" + tvOfDay;
            Integer offset = Math.toIntExact((dataTv - tvOfDay) / intervalTime);
            //初始化完整率统计详情
            if (!keyOffset.containsKey(redisKey) && Boolean.FALSE.equals(redisTemplate.hasKey(RedisKeys.getMeterLoadProfileIntegrity(redisKey)))) {
                int pointNumber = 86400 / intervalTime;
                int byteLen = pointNumber / 8;
                byte[] dataStatistic = new byte[byteLen];
                redisTemplate.opsForValue().set(RedisKeys.getMeterLoadProfileIntegrity(redisKey), new String(dataStatistic, StandardCharsets.UTF_8));
            }
            if (!keyOffset.containsKey(redisKey)) {
                keyOffset.put(redisKey, new HashSet<>());
            }
            keyOffset.get(redisKey).add(offset);
        }
        return keyOffset;
    }

    public Map<String, Set<Integer>> cacheMinutelyTimely(Map<String, Set<Long>> meterLpDataTvMap, String meterIdLpDataItemIdKey, Integer intervalTime, Integer spaceOfTime) {
        Set<Long> dataTvs = meterLpDataTvMap.get(meterIdLpDataItemIdKey);
        log.debug("save integrity: tv size={}, dataTv={}", dataTvs.size(), JSONUtil.toJsonStr(dataTvs));
        Map<String, Set<Integer>> keyOffset = new HashMap<>();
        for (Long dataTv : dataTvs) {
            Date date = new Date(dataTv * 1000);
            Long tvOfDay = DateUtils.truncate(date, Calendar.DATE).getTime() / 1000;
            String redisKey = meterIdLpDataItemIdKey + ":" + intervalTime + ":" + tvOfDay;
            Integer offset = Math.toIntExact((dataTv - tvOfDay) / intervalTime);
            //初始化及时率统计详情
            if (!keyOffset.containsKey(redisKey) && Boolean.FALSE.equals(redisTemplate.hasKey(RedisKeys.getMeterLoadProfileTimely(redisKey)))) {
                int pointNumber = 86400 / intervalTime;
                int byteLen = pointNumber / 8;
                byte[] dataStatistic = new byte[byteLen];
                redisTemplate.opsForValue().set(RedisKeys.getMeterLoadProfileTimely(redisKey), new String(dataStatistic, StandardCharsets.UTF_8));
            }
            if (!keyOffset.containsKey(redisKey)) {
                keyOffset.put(redisKey, new HashSet<>());
            }
            if (System.currentTimeMillis() / 1000 - dataTv <= spaceOfTime * 60) {
                keyOffset.get(redisKey).add(offset);
                log.debug("save timely : tv={}, offset={}, rediskey={}", dataTv, offset, redisKey);
            }
        }
        return keyOffset;
    }

}
